﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Numerics
{
    /// <summary>
    /// BigDecimal is modeled after Java's BigDecimal. The first four bytes is the scale,
    /// since that's how Java does it. The class uses int for scale and BitInteger for
    /// the unscaled value. Note this is different than .Net Decimal, which handles a
    /// smaller range of values.
    /// </summary>
    public class BigDecimal : IComparable<BigDecimal>, IEquatable<BigDecimal>
    {
        public static BigDecimal One = new BigDecimal(BigInteger.One, 0);
        public static BigDecimal Zero = new BigDecimal(BigInteger.Zero, 0);
        public static BigDecimal MinusOne = new BigDecimal(BigInteger.MinusOne, 0);

        public BigDecimal(BigInteger unscaledValue, int scale)
        {
            UnscaledValue = unscaledValue;
            Scale = scale;
        }
        public BigDecimal(int intValue)
            : this(new BigInteger(intValue),0)
        {
        }
        public BigDecimal(long longValue)
            : this(new BigInteger(longValue), 0)
        {
        }
        public BigDecimal(double doubleValue)
            : this(new BigInteger(doubleValue), 0)
        {
        }
        public BigDecimal(float floatValue)
            : this(new BigInteger(floatValue), 0)
        {
        }
        public BigDecimal(BigInteger bi)
            : this(bi, 0)
        {
        }
        public static BigDecimal ConvertFromDecimal(Decimal decimalValue)
        {
            byte[] bytes = FromDecimal(decimalValue);

            byte[] unscaledBytes = new byte[12];
            Array.Copy(bytes, unscaledBytes, unscaledBytes.Length);

            var unscaledValue = new BigInteger(unscaledBytes);
            var scale = bytes[14];

            if (bytes[15] == 128)
            {
                unscaledValue *= BigInteger.MinusOne;
            }

            BigDecimal bigDec = new BigDecimal(unscaledValue,scale);
            return bigDec;
        }
        /// <summary>
        /// This method is borrowed from FluentCassandra. No sense reinventing it.
        /// </summary>
        /// <param name="d"></param>
        /// <returns></returns>
        private static byte[] FromDecimal(decimal d)
        {
            byte[] bytes = new byte[16];

            int[] bits = decimal.GetBits(d);
            int lo = bits[0];
            int mid = bits[1];
            int hi = bits[2];
            int flags = bits[3];

            bytes[0] = (byte)(lo);
            bytes[1] = (byte)(lo >> 0x8);
            bytes[2] = (byte)(lo >> 0x10);
            bytes[3] = (byte)(lo >> 0x18);
            bytes[4] = (byte)(mid);
            bytes[5] = (byte)(mid >> 0x8);
            bytes[6] = (byte)(mid >> 0x10);
            bytes[7] = (byte)(mid >> 0x18);
            bytes[8] = (byte)(hi);
            bytes[9] = (byte)(hi >> 0x8);
            bytes[10] = (byte)(hi >> 0x10);
            bytes[11] = (byte)(hi >> 0x18);
            bytes[12] = (byte)(flags);
            bytes[13] = (byte)(flags >> 0x8);
            bytes[14] = (byte)(flags >> 0x10);
            bytes[15] = (byte)(flags >> 0x18);

            return bytes;
        }
        /// <summary>
        /// Method expects the first 4 bytes to be the scale and the
        /// remaining bytes the BigInteger value. Note this doesn't
        /// follow System.Decimal convention, which puts the scale
        /// at the end.
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static BigDecimal ConvertFromBytes(byte[] bytes)
        {
            byte[] scaleBytes = new byte[4];
            Array.Copy(bytes, 0, scaleBytes, 0, 4);
            Array.Reverse(scaleBytes);
            byte[] unscaledBytes = new byte[bytes.Length - 4];
            Array.Copy(bytes, scaleBytes.Length, unscaledBytes, 0, unscaledBytes.Length);
            Array.Reverse(unscaledBytes);

            BigDecimal bigDec = new BigDecimal(new BigInteger(unscaledBytes), BitConverter.ToInt32(scaleBytes,0));
            return bigDec;
        }
        public int Scale
        {
            get;
            set;
        }
        public BigInteger UnscaledValue
        {
            get;
            set;
        }
        public bool IsEven
        {
            get
            {
                return UnscaledValue.IsEven;
            }
        }
        public bool IsOne
        {
            get
            {
                return UnscaledValue.IsOne;
            }
        }
        public bool IsPowerOfTwo
        {
            get
            {
                return UnscaledValue.IsPowerOfTwo;
            }
        }
        public bool IsZero
        {
            get
            {
                return UnscaledValue.IsZero;
            }
        }
        public int Sign
        {
            get
            {
                return UnscaledValue.Sign;
            }
        }
        /// <summary>
        /// The byte[] array uses the first 4 bytes for the scale and the
        /// remaining bytes for BigInteger value
        /// </summary>
        /// <returns></returns>
        public byte[] ToByteArray()
        {
            byte[] scale = BitConverter.GetBytes(Scale);
            byte[] unscaledValue = UnscaledValue.ToByteArray();

            Array.Reverse(scale);
            Array.Reverse(unscaledValue);

            byte[] bytes = new byte[unscaledValue.Length + scale.Length];
            Array.Copy(scale, 0, bytes, 0, scale.Length);
            Array.Copy(unscaledValue, 0, bytes, scale.Length, unscaledValue.Length);
            return bytes;
        }
        #region Conversion Methods
        public int ToInt()
        {
            return Convert.ToInt32(UnscaledValue);
        }
        public long ToLong()
        {
            return Convert.ToInt64(UnscaledValue);
        }
        public float ToSingle()
        {
            return Convert.ToSingle(UnscaledValue);
        }
        public double ToDouble()
        {
            return Convert.ToDouble(UnscaledValue);
        }
        public Decimal ToDecimal()
        {
            return Convert.ToDecimal(UnscaledValue);
        }
        public override string ToString()
        {
            if (Scale == 0)
            {
                return UnscaledValue.ToString();
            }
            else
            {
                char[] coeff;
                int offset;  // offset is the starting index for coeff array
                // Get the significand as an absolute value
                offset = 0;
                coeff = UnscaledValue.ToString().ToCharArray();
                
                StringBuilder buf = new StringBuilder();
                int coeffLen = coeff.Length - offset;
                long adjusted = -(long)Scale + (coeffLen -1);
                if ((Scale >= 0) && (adjusted >= -6)) // plain number
                {
                    int pad = Scale - coeffLen;         // count of padding zeros
                    if (pad >= 0)
                    {                     // 0.xxx form
                        buf.Append('0');
                        buf.Append('.');
                        for (; pad > 0; pad--)
                        {
                            buf.Append('0');
                        }
                        buf.Append(coeff, offset, coeffLen);
                    }
                    else
                    {                         // xx.xx form
                        buf.Append(coeff, offset, -pad);
                        buf.Append('.');
                        buf.Append(coeff, -pad + offset, Scale);
                    }
                }
                return buf.ToString();
            }
        }
        #endregion

        #region Operators
        public static bool operator ==(BigDecimal left, BigDecimal right)
        {
            return left.Equals(right);
        }
        public static bool operator !=(BigDecimal left, BigDecimal right)
        {
            return !left.Equals(right);
        }
        public static bool operator >(BigDecimal left, BigDecimal right)
        {
            return (left.CompareTo(right) > 0);
        }
        public static bool operator >=(BigDecimal left, BigDecimal right)
        {
            return (left.CompareTo(right) >= 0);
        }
        public static bool operator <(BigDecimal left, BigDecimal right)
        {
            return (left.CompareTo(right) < 0);
        }
        public static bool operator <=(BigDecimal left, BigDecimal right)
        {
            return (left.CompareTo(right) <= 0);
        }
        #endregion

        #region IComparable<BigDecimal>
        public int CompareTo(BigDecimal right)
        {
            var UnscaledValueCompare = UnscaledValue.CompareTo(right.UnscaledValue);
            var ScaleCompare = Scale.CompareTo(right.Scale);

            if (UnscaledValueCompare == ScaleCompare)
            {
                return UnscaledValueCompare;
            }
            if (ScaleCompare == 0)
            {
                return UnscaledValueCompare;
            }
            var ScaledValue = BigInteger.Divide(UnscaledValue, BigInteger.Pow(new BigInteger(10), Scale));
            var OtherScaledValue = BigInteger.Divide(right.UnscaledValue, BigInteger.Pow(new BigInteger(10), right.Scale));

            return ScaledValue.CompareTo(OtherScaledValue); 
        }
        #endregion

        #region IEquatable<BigDecimal>
        public bool Equals(BigDecimal right)
        {
            return Scale == right.Scale && UnscaledValue.Equals(right.UnscaledValue);
        }
        #endregion
        /// <summary>
        /// We use the unscaled and scaled value hashCode
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return UnscaledValue.GetHashCode() ^ Scale.GetHashCode();
        }
        public override bool Equals(object obj)
        {
            if (obj is BigDecimal)
            {
                return false;
            }
            else
            {
                return Equals((BigDecimal)obj);
            }
        }
    }
}
